动态代理说大不大,说小不小,可深可浅。往深了说还是对JVM的了解程度要足够深入,时间篇幅有限,本文专注于回答如下问题,不作更深入的探讨。
- JDK和Cglib动态代理,分别怎么使用
- JDK动态代理的原理
- Cglib动态代理的原理
- 为什么JDK动态代理一定要实现接口,而Cglib就不用?
- JDK和Cglib,本质上有什么区别?
JDK动态代理
使用
一个简单的场景
- 一个Service接口,拥有sayHello()方法
- 一个ServiceImpl实现类,实现Service
- 创建一个ServiceImpl的代理类,代理sayHello()方法,在调用原方法的前后,打印锚点
例子如下
| 1 | interface Service { | 
总结一下,要点
- 被代理的类要实现接口
- 代理的逻辑要通过InvocationHandler实现
- Proxy.newProxyInstance()生成代理类,需要提供类加载器、被代理的接口、InvocationHandler
原理
源码自己跟,最终会来到核心方法
- java.lang.reflect.Proxy.ProxyBuilder#defineProxyClass- 关键逻辑:生成类的字节码流;使用sun.misc.Unsafe直接从流创建Class对象。 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13- private static Class<?> defineProxyClass(Module m, List<Class<?>> interfaces) { 
 
 ...
 byte[] proxyClassFile = ProxyGenerator.generateProxyClass(proxyName, interfaces.toArray(EMPTY_CLASS_ARRAY), accessFlags);
 try {
 Class<?> pc = UNSAFE.defineClass(proxyName, proxyClassFile, 0, proxyClassFile.length, loader, null);
 return pc;
 } catch (ClassFormatError e) {
 ...
 }
 ...
 }
- java.lang.reflect.ProxyGenerator#generateProxyClass(java.lang.String, java.lang.Class<?>[], int),这里可以看到- 这里是类字节码流生成的关键逻辑:凭空构建一个class流 - 常规class字节码文件的组成:魔数、版本号、常量池等
- 写入父类:固定为"java/lang/reflect/Proxy"
- 写入需要被代理的接口,用户传入的
- 写入字段和方法,这其中包含了我们传入的InvocationHandler
 - 1 
 2
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43- ByteArrayOutputStream bout = new ByteArrayOutputStream(); 
 DataOutputStream dout = new DataOutputStream(bout);
 dout.writeInt(0xCAFEBABE);
 // u2 minor_version;
 dout.writeShort(CLASSFILE_MINOR_VERSION);
 // u2 major_version;
 dout.writeShort(CLASSFILE_MAJOR_VERSION);
 cp.write(dout); // (write constant pool)
 // u2 access_flags;
 dout.writeShort(accessFlags);
 // u2 this_class;
 dout.writeShort(cp.getClass(dotToSlash(className)));
 // u2 super_class;
 dout.writeShort(cp.getClass(superclassName));
 // u2 interfaces_count;
 dout.writeShort(interfaces.length);
 // u2 interfaces[interfaces_count];
 for (Class<?> intf : interfaces) {
 dout.writeShort(cp.getClass(
 dotToSlash(intf.getName())));
 }
 // u2 fields_count;
 dout.writeShort(fields.size());
 // field_info fields[fields_count];
 for (FieldInfo f : fields) {
 f.write(dout);
 }
 // u2 methods_count;
 dout.writeShort(methods.size());
 // method_info methods[methods_count];
 for (MethodInfo m : methods) {
 m.write(dout);
 }
 // u2 attributes_count;
 dout.writeShort(0); // (no ClassFile attributes for proxy classes)
通过设置系统属性:System.setProperty("jdk.proxy.ProxyGenerator.saveGeneratedFiles", "true")可以将生成的字节码保存为文件,然后反编译看结果
| 1 | package com.sun.proxy; | 
几个要点
- 该代理类直接继承了Proxy类,实现类我们指定的Service接口
- sayHello()代理的原理:调用- InvocationHandler.invoke()完成实际调用
- 代理类的所有方法,都会调用InvocationHandler.invoke()
小结
JDK的动态代理,是从头构建新的类字节码流,然后加载到JVM中达成的。其使用方法必须依赖接口、InvocationHandler、Proxy,并不是非常方便。
CGlib
使用
类似上面,一个简单的场景
- 一个Service类,不用实现任何接口
- 创建Service的代理类,代理sayHello()方法,在调用原方法的前后,打印锚点
| 1 | open class PersonService { | 
总结一下,要点
- 只需要被代理类自己,但被代理类和方法必须是open的,即可被继承和覆盖的
- 使用Enhancer类,方法拦截使用MethodInterceptor定义代理逻辑
原理
同样,跟跟源码,发现关键逻辑在:net.sf.cglib.proxy.Enhancer#generateClass,这里是通过ASM库来生成类字节码的,过程比较复杂,需要对ASM API比较了解才能分析,这里这里暂时忽略。直接看生成的代码。
设置系统属性System.setProperty(DebuggingClassWriter.DEBUG_LOCATION_PROPERTY, "xxx")可以将生成的代理类输出。
| 1 | public class PersonService$$EnhancerByCGLIB$$470f9603 extends PersonService implements Factory { | 
看到
- 生成的代理类直接继承了Service类
- 方法拦截都是通过MethodInterceptor
- 构建MethodProxy传入,用于真实方法的调用。
注意:MethodInterceptor中如果直接调用Method,会造成堆栈溢出。必须通过MethodProxy.invokeSuper()方法调用才行。
小结
CGlib是基于ASM进行字节码生成的,在使用上会简单很多。
区别
可以看到,无论是JDK动态代理,还是CGlib,最终都是生成了代理类的字节码,并将其加载为新的类。从这个角度上看,貌似没啥区别呀?理论上,JDK的动态代理也可以设计成CGlib那样,直接基于类生成代理子类,就像有人做的那样。很多人说,JDK动态代理只能基于接口,是因为代理类继承了Proxy,而Java是单继承,没有办法再继承用户自定义类,我认为这个说法因果倒置了,都说了,如果想要继承自定义类,是能够办到的。对于这个问题,我的看法是JDK设计者故意为之,至于原因嘛,我也不大说得上来(说到底,还是菜)。
JDK代理和CGlib代理的区别,除了API使用上,更重要的是字节码生成方式上的区别:前者凭空生成;后者使用ASM基于被代理类生成。
都是生成,区别在于生成的效率以及生成的代理类的效率。这又涉及到谁效率高的问题了。用JMH大概试一试吧。我们用几乎一样的被代理类,生成代理类,调用方法。测试每个操作所耗费的时间。
| 1 | ///////////////////////CGlib的测试case | 
对于上面的case,当只保留代理类创建逻辑时,测试结果
| 1 | Benchmark (count) Mode Cnt Score Error Units | 
当同时保留创建和方法调用逻辑时,测试结果
| 1 | Benchmark (count) Mode Cnt Score Error Units | 
当前这样的基准测试是不准确的,但还是大致可以得出代理类的创建CGlib比JDK快,但调用上JDK更快。快多少?快不了多少。
总结
按照本文的方式来探索动态代理,还是远远不够的,要想把这一块理解透彻,说到底,还是要对JVM有深入的研究,也就是说,还需要继续探索的点
- ASM库的使用、深入理解 
- 类加载的深入研究,ClassLoader类的剖析 
- sun.misc.Unsafe类的深入研究 
- JVM的深入研究